package co.recloud.ariadne.store;

import java.io.*;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * Created by IntelliJ IDEA.
 * User: alex
 * Date: 11/19/11
 * Time: 7:06 PM
 * To change this template use File | Settings | File Templates.
 */
public class FileStore<T> implements GridBlockStore<T> {
    private File file;
    private RandomAccessFile stream;
    private int width;
    private int height;
    private int blockSize;
    private ReadWriteLock[][] latches;
    private boolean [][] isOpen;

    public FileStore(File file, int blockSize, int width, int height) {
        this.file = file;
        this.blockSize = blockSize + 4;
        this.width = width;
        this.height = height;
        isOpen = new boolean[width][height];
        if (file.exists()) {
            setupStreams();
            readFileHeader();
        } else {
            try {
                file.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
            }
            setupStreams();
            createFileHeader();
            createBlockHeaders();
        }
        latches = new ReadWriteLock[width][height];
        for (int x = 0; x < width; x++) {
            for (int y = 0; y < width; y++) {
                long address = locationToAddress(x, y);
                latches[x][y] = new ReentrantReadWriteLock();
                isOpen[x][y] = true;
            }
        }
    }

    private void createBlockHeaders() {
        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                long address = locationToAddress(x, y);
                try {
                    stream.seek(address);
                    stream.write(0); //Total block size

                } catch (IOException e) {
                    e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
                }
            }
        }
    }

    private void setupStreams() {
        try {
            stream = new RandomAccessFile(file, "rws");
        } catch (FileNotFoundException e) {
            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
        }
    }

    private void readFileHeader() {
        try {
            blockSize = stream.read();
            width = stream.read();
            height = stream.read();
        } catch (IOException e) {
            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
        }
    }

    private void createFileHeader() {
        try {
            stream.write(blockSize);
            stream.write(width);
            stream.write(height);
        } catch (IOException e) {
            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
        }
    }

    public T readBlock(int x, int y) {
        T rows = null;
        byte [] data = readRawBlock(x, y);
        Lock latch = latches[x][y].readLock();
        try {
            latch.lockInterruptibly();

            if(data.length > 0) {
                try {
                        ObjectInputStream objIn = new ObjectInputStream(new ByteArrayInputStream(data));
                        try {
                            rows = (T) objIn.readObject();
                        } catch (ClassNotFoundException e) {
                            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
                        }
                } catch (IOException e) {
                    System.out.println("IO excepion");
                    e.printStackTrace();
                }
            } else {
                rows = null;
            }
        } catch (InterruptedException e) {
            System.out.println("Interrupted!");
            e.printStackTrace();
        }

        return rows;
    }

    public void writeBlock(int x, int y, T rows) {
        Lock latch = latches[x][y].writeLock();
        try {
            latch.lockInterruptibly();
            ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
            try {
                ObjectOutputStream objectOut = new ObjectOutputStream(bytesOut);
                objectOut.writeObject(rows);
                byte[] data = bytesOut.toByteArray();
                writeRawBlock(x, y, data);
            } catch (IOException e) {
                e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
            } finally {

            }
        } catch (InterruptedException e) {
            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
        }
    }

    private long locationToAddress(int x, int y) {
        long address = 12 + (y * width + x) * blockSize;
        return address;
    }

    public void writeRawBlock(int x, int y, byte [] data) {
        long address = locationToAddress(x, y);
        Lock latch = latches[x][y].writeLock();
        isOpen[x][y] = false;
        try {
            latch.lockInterruptibly();
            try {
                int totalSize = data.length;
                int remainingSize = totalSize;
                stream.seek(address);
                stream.write(totalSize);
                while (remainingSize > 0) {
                    int addedSize = Math.min(blockSize, remainingSize);
                    if(remainingSize == totalSize) {
                        addedSize -= 4;
                    }
                    stream.write(data, (totalSize - remainingSize), addedSize);
                    remainingSize -= addedSize;
                    if(remainingSize  > 0) {
                        address += (width * height - 1) * blockSize;
                        stream.seek(address);
                    }
                }

            } catch (IOException e) {
                e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
            } finally {

            }
        } catch (InterruptedException e) {
            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
        } finally {
            latch.unlock();
        }
    }
    public byte[] readRawBlock(int x, int y) {
        long address = locationToAddress(x, y);
        Lock latch = latches[x][y].readLock();
        byte[] data = null;
        try {
            latch.lockInterruptibly();
            try {
                stream.seek(address);
                int totalSize = stream.read();
                int remainingSize = totalSize;
                data = new byte[totalSize];
                while (remainingSize > 0) {
                    int addedSize = Math.min(blockSize, remainingSize);
                    if(remainingSize == totalSize) {
                        addedSize -= 4;
                    }
                    stream.read(data, (totalSize - remainingSize), addedSize);
                    remainingSize -= addedSize;
                    if(remainingSize > 0) {
                        address += (width * height - 1) * blockSize;
                        stream.seek(address);
                    }
                }
            } catch (IOException e) {
                System.out.println("IO Exception");
                e.printStackTrace();
            }
        } catch (InterruptedException e) {
            System.out.println("Interrupted!");
            e.printStackTrace();
        } finally {
            latch.unlock();
        }
        return data;
    }
    
    public int[] getOpenBlock() {
        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++ ) {
                if(isOpen[x][y]){
                    int[] coords = new int[2];
                    coords[0] = x;
                    coords[1] = y;
                    return coords;
                }
            }
        }
        return null;
    }
    
    public void freeBlock(int x, int y) {
        try {
            latches[x][y].writeLock().lockInterruptibly();
            isOpen[x][y] = true;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            latches[x][y].readLock().unlock();
        }
    }
}
